Chapter 16 Python Exceptions

Note the following:-

  1. This html document is meant as an accompaniment to Chapter 16 Python Exceptions.
  2. The document contains scripts executed on IDLE as well as on Jupyter notebook.
  3. The scripts executed on Jupyter can be directly copied and run into a Jupyter notebook or some other IDE (Like Pycharm or Eclipse with PyDev or Visual studio).
  4. However the scripts on IDLE also contain the >>> symbol and therefore cannot be directly executed. If you want to execute them on IDLE or Jupyter, you need to manually remove the >>> symbol.
  5. Wherever needed some background material from the book is also included to help you better understand the scripts
  6. The topic numbers given on each paragraph match the topic numbers of the book, so you can easily identify the topics and corresponding scripts.
  7. In some of the scripts, the file paths give are that of the author's computer. You need to replace them with file paths of your own computer.
  8. At some places, to improve readability, page numbers of the book are indicated in green font like:- See Page 181 of the book
  9. This document was first created as a Jupyter Notebook as combination of Markdown and code cells (extension .ipynb) and then downloaded as html. If someone wants to "modify" or "extend' this document, you may ask for the original .ipynb file by sending me an e-mail at:- 999.anuraggupta@gmail.com

16.2. Basic concepts of exceptions in Python
16.2.1. Errors versus exceptions
Take example of a simple IndexError which is raised when you try to access an index of a sequence which is larger than the largest index in the sequence. This will be clear from the following code:
This script is available on page 393 of the book

>>> myS = 'abcd'
>>> myS[4]
Traceback (most recent call last):
  File "<pyshell#17>", line 1, in<module>
    myS[4]
IndexError: string index out of range

16.2.2. The raise statement

In the example given above, it was the Python interpreter which raised the exception. However a programmer can himself raise an exception by using the raise statement. An exception can have an argument, which is a value that gives additional information about the problem. The content of the argument vary with exception. Syntax for exception statement is as follows (Note that the argument is optional):

raise [exceptionName[, argument]]

This is clarified in the following example:

>>>raise NameError('I raised this error')
Traceback (most recent call last):
  File "<pyshell#18>", line 1, in<module>
raise NameError('I raised this error')
NameError: I raised this error
>>>

16.2.3 The try-except-else block of code in Python

If there is a piece of code which might throw an exception, then the good way to deal with this code would be to enclose it in a try block of code. The try block gives the programmer an opportunity to deal with the error. So if you have a try block and you get an error, the interpreter will give you an opportunity to deal with it in the except block. However if the script in the except block is unable to deal with the error, then the Python interpreter will stop the execution of the script and raise an in-built error. When you enclose a suspicious code in a try block, there are following two possibilities:

  • Possibility 1 is that it will throw an exception.
  • Possibility 2 is that it will not throw an exception.

You can deal with this as shown in the following code: This script is available on page 394 of the book

# Pseudo code
try:
    # Code with bugs... could throw exception
except(Exception1[, Exception2 [, ... ExceptionN]]):
    # If any of above exceptions ie Exception1 to
    # ... ExceptionN occur, execute this block
else:
    # If there is no exception in the list of exceptions, then execute this code

You can use a try-except block (without else-finally) also to ensure that a user gives a particular type of input. For example, if you want the user to only give an integer, you may write code as follows on IDLE:
This script is available on page 395 of the book

In [1]:
while True:
    try:
        myI = int(input("Give an integer.."))
        print("You gave integer... so quit")
        break
    except ValueError:
        print("Not an integer... try again")
Give an integer..a
Not an integer... try again
Give an integer..3
You gave integer... so quit

16.2.4. The try-except-else-finally block with multiple except and except with no exception type

Note that when you are writing the except block you have the following three options:

  • Option 1 you write one type of exception.
  • Option 2 you write more than one types of exception.
  • Option 3 you do not write any type of exception.

So for except block with one type of exception, the block is executed only if that type of exception is raised. For except block with multiple exceptions, the except block is executed if any of the exceptions in the exceptions given is raised. For except block with no exception, the block is executed if there is any type of exception. This will become clear with the following example:
This script is available on page 396 of the book

In [2]:
def myFunc(myL, idx, divident, divisor):
    try:
        print('If this is printed, index OK->',myL[idx])
        print('If this printed, divisor not 0->',divident/ divisor)
    except IndexError:
        print('Index is out of range')
    except ZeroDivisionError:
        print(' Cant divide by 0')
    else:
        print('No exception raised')
    finally:
        print('Exception or not, this will be printed')  

The above code has been saved in a file named test4.py. You can execute this code with different inputs as shown:
This script is available on page 397 of the book

>>>import test4
>>> L = ['a', 'b', 'c', 'd']
>>> test4.myFunc(L, 2, 8, 2)
If this is printed, index OK-> c
If this printed, divisor not 0->4.0
No exception raised
Exception or not, this will be printed
>>> test4.myFunc(L,5,8,2)
Index is out of range
Exception or not, this will be printed
>>> test4.myFunc(L,2,8,0)
If this is printed, index OK-> c
 Cant divide by 0
Exception or not, this will be printed
>>>

You can slightly modify the above program and introduce a line of code which throws an exception which is not dealt with in the except block as follows (note the code is saved as a module test4.py).
This script is available on page 398 of the book

In [3]:
def myFunc(myL, idx, divident, divisor):
    try:
        print('If this is printed, index OK',myL[idx])
        print('If this printed, divisor not 0',divident/ divisor)
        print(myL + idx)   # Adding list myL to int idx raises TypeError
        # .. which is not handled in except block
    except IndexError:
        print('Index is out of range')
    except ZeroDivisionError:
        print(' Cant divide by 0')
    else:
        print('No exception raised')
    finally:
        print('Exception or not, this will be printed')

The new line of code added is

print(myL + idx)

It contains a print function which prints the result of addition of myL with idx. Now myL is a list object while idx is an integer object and the two cannot be added. So this line of code will throw an error of type TypeError, but this type of error is not handled in the two except blocks. So what will happen. First the finally block of code will be executed, and second the script will raise the built-in error of type TypeError as follows:
This script is available on page 399 of the book

>>>import test4
>>> L = [1, 2, 3, 4]
>>> test4.myFunc(L, 1, 4, 2)
If this is printed, index OK 2
If this printed, divisor not 0 2.0
Exception or not, this will be printed
Traceback (most recent call last):
  File "<pyshell#2>", line 1, in<module>
    test4.myFunc(L, 1, 4, 2)
  File "C:/Users/ADG/AppData/Local/Programs/Python/Scripts\test4.py", line 5, in myFunc
print(myL + idx)   # Adding list myL to int idx raises TypeError
TypeError: can only concatenate list (not"int") to list
>>>

You can modify the above code to take care of the TypeError generated (this script is saved as test5.py):
This script is available on page 399 of the book

In [4]:
def myFunc(myL, idx, divident, divisor):
    try:
        print('If this is printed, index OK',myL[idx])
        print('If this printed, divisor not 0',divident/ divisor)
        print(myL + idx)    # Adding list myL to int idx raises TypeError
        # .. which is not handled in except block
    except IndexError: # Catches only IndexError
        print('Index is out of range')
    except ZeroDivisionError: # Catches only ZeroDivisionError
        print(' Cant divide by 0')
    except:                     # Catches ALL exceptions
        print('All errors are now taken care of')
    else:
        print('No exception raised')
    finally:
        print('Exception or not, this will be printed')

You may execute this file (By importing test5.py and then using its myFunc()) as follows:

>>> myFunc([1,2,3], 2, 4, 1)
If this is printed, index OK 3
If this printed, divisor not 04.0
All errors are now taken care of
Exception or not, this will be printed
>>>

16.2.5. Using try-except block to read a file

Reading a file may not always be successful leading to errors and therefore premature termination of the program. You can write a function which reads a file and if there is an error, it catches the exception. The function has

  • a try block,
  • an except block and
  • an else block.

This script is available on page 400 of the book

In [5]:
def myReader(fileName):
    try:
        with open(fileName, 'r+') as f:
            fContent = f.read()
            print(fContent)
    except IOError:
        print("Something wrong")
    else:
        print("ok")
# Call the function
fName = input("Give file name:- ")
myReader(fName)
Give file name:- r'C:\temp_data\volcanoes.txt'
Something wrong

16.3. User-defined exceptions

Sometimes a user needs to create his own exceptions. Python allows the user to derive his own exceptions. In older Python versions, there were following two ways in which exceptions could be derived.

  • One version of exceptions was derived from exception class.
  • other from the string class.

But from Python 2.6 onwards, it is possible to derive exceptions only from exception class and not from string class.

Nowadays, if you want to create a user-defined exception then you have to derive or inherit it from Exception class or from a class which in turn has been inherited from some exception class.
This script is available on page 401 of the book

In [6]:
class MyError(Exception):
    print("User Exception")

# raise exception MyError
try:
    print("entering the try block")
    raise MyError
    print("This is not printed")# This line is never executed
except MyError:
    print("raised")
User Exception
entering the try block
raised

16.4.1. An except clause may name multiple exceptions as a parenthesized tuple

From Python 3.x onwards, you may raise a number of exceptions with the same except statement. But the exceptions must be in the form of a “parenthesized tuple”.

Suppose you have some errors, say RuntimeError, TypeError and NameError, then you could raise these three errors in a single except as a “tuple of errors” as follows:

except (RuntimeError, TypeError, NameError):
    pass

16.7. Beyond text book
See Page 403 of the book

16.7.1. The exception hierarchy

In Python, exceptions also have a class hierarchy. Study the hierarchy of exceptions available at: https://docs.python.org/3/library/exceptions.html#exception-hierarchy . After studying the class hierarchy of exceptions, you may realize the following:

  • At the top of the hierarchy is the class BaseException. So, all exceptions in Python ultimately inherit from this class.
  • The four major children (or sub-classes) of this class BaseException are: SystemExit, KeyboardInterrupt, GeneratorExit and Exception.
  • If you intend to create your own exceptions, then you should use subclass (i.e., inherit from) Exception class and never from BaseException.

The important things to understand about hierarchy of exceptions are that:-

  • An exception of a “parent” class will also “catch” all the exceptions of its “child or derived” classes.
  • So when using “except” statement to “catch” an error, you should always “catch” an error of “derived” class before an error of the “parent” class.
  • Should you “catch” an exception of a “parent” class before an excetion of a “derived” class, then the exception of the derived class will never be caught.

This is demonstrated in the following code:
This script is available on page 404 of the book

In [7]:
class A(Exception):
    pass
class B(A):
    pass

# Raise B before A
print('Raise B before A')
for x in [A, B]:
    try:
        raise x()
    except B:
        print('Catch B') # Cant catch A. So A can be reached
    except A:
        print('Catch A')

# Raise A before B
print('Raise A before B')
for x in [A, B]:
    try:
        raise x()
    except A: # Can catch B. So B never reached.
        print('Catch A')
    except B:
        print('Catch B')
Raise B before A
Catch A
Catch B
Raise A before B
Catch A
Catch A

16.7.2. The exception object

As pointed out in the previous section, all exceptions are classes in Python, so instances of exceptions are objects. You can access the exception objects as follows:

Note that in the above code, the error_object will refer to one of the three error classes given in the tuple because only one of the three errors can be raised at one time. Further, the lifetime of this error_object is only the indented block of code following the except statement.

Within this block you can use this error_object. The following code shows the use of this error_object (by convention this error_object is often called e)
This script is available on page 405 of the book

In [8]:
try:
    x = 1/0
except ZeroDivisionError as e:
    print('type of e->', type(e))
    print('arguments of e->', e.args)
    print('string representation of e->', str(e))
type of e-> <class 'ZeroDivisionError'>
arguments of e-> ('division by zero',)
string representation of e-> division by zero

16.8. Assignment – studying the traceback module

You need to understand that when an exception is thrown in a Python script, the following happen:-

  • A Traceback object is created. This Traceback object is by convention called tb.
  • You can “access or get hold of” this Traceback object using sys.exc_info() function of the sys module. (How to “access” the Traceback object using sys.exc_info() function is explained below).
  • Once you get this Traceback object (By using the sys.exc_info() function), then you can access its various attributes/ methods by using functions of the traceback module. (Note we use “Traceback” to refer to an object of this class and “traceback” to refer to the traceback module).
  • So by using two Python modules namely (1) sys and (2) traceback, you can “access” the Traceback object created and also “access” certain attributes of this Traceback object.
  • Do note that the Traceback object (conventionally called tb) is different from the traceback module. The Traceback object (that is tb) will always be “implicitly created” when an exception is thrown. On the other hand the traceback module is a module provided to programmers to extract relevant information out of the Traceback object tb. So a Traceback object is created by the Python interpreter whenever an exception is thrown. On the otherhand a programmer may use the traceback module to “do something” with a Traceback object.

So to do this assignment you need to:-

  • Access a Traceback object using sys.exc_inf() function inside the exception handler.
  • Then use the functions of the traceback module to get information about this Traceback object

Python has a module named traceback. As per the official documentation , “This module provides a standard interface to extract, format and print stack traces of Python programs”.

The assignment is to study this module and use it for getting information about exceptions raised.

Python also has a sys module which has a sys.exc_info() method. Together these two modules can be used to get information about the exception being handled.

The signature of the sys.exe_info() method on Jupyter is:

Docstring:
exc_info() -> (type, value, traceback)
Return information about the most recent exception caught by an except clause in the current stack frame or in an older stack frame.
Type:      builtin_function_or_method

So this method returns a tuple of three values namely (1) type (2) value and (3) traceback

  • type:- This is the first item in the tuple (that is at index 0) and it contains the “type” of exception being handles
  • value:- This is the second item in the tuple (that is at index 1) and contains arguments that are being passed to constructor of exception class. (This will become clearer with the following example)
  • traceback:- This is the third item in the tuple (that is at index 2). It contains a “Traceback object”.

It is this Traceback object which is of interest because it “encapsulates” the call stack at the point where the exception originally occurred. It is this third parameter (index 2) which will be used with the traceback module.

The following script shows how the sys.exc_info() method is used:
This script is available on page 407 of the book

In [9]:
import sys
try:
    a = 1/0
except ZeroDivisionError as e:
    exc_tup = sys.exc_info()
except_type = exc_tup[0]
print('exception type->', except_type)
except_value = exc_tup[1]
print('exception value->', except_value)
except_obj = exc_tup[2]
print('exception object type->', type(except_obj))
print('exception object->', except_obj)
exception type-> <class 'ZeroDivisionError'>
exception value-> division by zero
exception object type-> <class 'traceback'>
exception object-> <traceback object at 0x03C7FD00>

Now coming to the module traceback, it has a function traceback.extract_tb().

This function takes as its argument a traceback object (which is the third parameter of the tuple returned by sys.exc_info() and generally called “tb”). The signature of this function is as follows:

Signature: traceback.extract_tb(tb, limit=None)
Docstring:
Return list of up to limit pre-processed entries from traceback.
A pre-processed stack trace entry is a quadruple (filename, line number, function name, text) representing the information that is usually printed for a stack trace.  The text is a string with leading and trailing whitespace stripped; if the source is not available it is None.

So out of the four stack trace entries, you will get entries at index 0, 1 and 2 but the entry at index 3 may or may not exist (since it can be None). The following script is an example of how sys.exc_info() and traceback.extract_tb can be used to get information about the exception caught by the except clause.
This script is available on page 408 of the book

In [10]:
import sys
import traceback
try:
    x = 1/0
except ZeroDivisionError as e:
    # Get current system exception
    e_type, e_value, e_tb = sys.exc_info()
    print('e_type->', e_type)
    print('e_value->', e_value)
    
    # Use extraxt_tb() to 
    tb_stack = traceback.extract_tb(e_tb)
    for tb_frame in tb_stack:
        print('tb_frame->', tb_frame)
        print('type of tb_frame->', type(tb_frame))
        func_name = tb_frame[2]
        lineno = tb_frame[1]
        filename = tb_frame[0]
        print('func_name->', func_name)
        print('lineno->', lineno)
        print('filename->', filename)
    # You can also use traceback.extract_stack() to get info about the error
    print(traceback.extract_stack()) 
e_type-> <class 'ZeroDivisionError'>
e_value-> division by zero
tb_frame-> <FrameSummary file <ipython-input-10-58729395e87b>, line 4 in <module>>
type of tb_frame-> <class 'traceback.FrameSummary'>
func_name-> <module>
lineno-> 4
filename-> <ipython-input-10-58729395e87b>
[<FrameSummary file C:\ProgramData\Anaconda3\lib\runpy.py, line 193 in _run_module_as_main>, <FrameSummary file C:\ProgramData\Anaconda3\lib\runpy.py, line 85 in _run_code>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py, line 16 in <module>>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\traitlets\config\application.py, line 658 in launch_instance>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\ipykernel\kernelapp.py, line 477 in start>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\zmq\eventloop\ioloop.py, line 177 in start>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\tornado\ioloop.py, line 888 in start>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\tornado\stack_context.py, line 277 in null_wrapper>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\zmq\eventloop\zmqstream.py, line 440 in _handle_events>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\zmq\eventloop\zmqstream.py, line 472 in _handle_recv>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\zmq\eventloop\zmqstream.py, line 414 in _run_callback>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\tornado\stack_context.py, line 277 in null_wrapper>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\ipykernel\kernelbase.py, line 283 in dispatcher>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\ipykernel\kernelbase.py, line 235 in dispatch_shell>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\ipykernel\kernelbase.py, line 399 in execute_request>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\ipykernel\ipkernel.py, line 196 in do_execute>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\ipykernel\zmqshell.py, line 533 in run_cell>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py, line 2698 in run_cell>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py, line 2802 in run_ast_nodes>, <FrameSummary file C:\ProgramData\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py, line 2862 in run_code>, <FrameSummary file <ipython-input-10-58729395e87b>, line 23 in <module>>]